Override the context for timer funcs
As discussed here Context is incorrect when calling from a timer the timer is outside the context of the main thread of blender.
To get around this pass the objects to the function when defined, and use these in a context override of any operators.
A quick test reveals that the animation cancell operator requires context members "window" and "screen"
import time, bpy, functools
from bpy import context
def stop(w):
print("STOP")
c = {"window" : w,
"screen" : w.screen
}
bpy.ops.screen.animation_cancel(c, restore_frame=False)
return None
#f = functools.partial(loop, bpy.context.screen.name)
if __name__ == '__main__':
window = bpy.context.window
bpy.ops.screen.animation_play()
bpy.app.timers.register(functools.partial(stop, window), first_interval=30)
The same method could probably be employed with threading.Timer
As a rule of thumb, threading and blender don't go together too well. The new timer app was created to alleviate this somewhat. But since it is still a thread outside blenders main thread it has issues with keeping context.
Please notice the way the index of the window is passed in answer to linked question. Simply passing the window as shown here could be catastrophic if the window is closed, and hence the reference lost, between invoking timer and it firing.