[time] Support timedeltas in timeout()

Change-Id: Id91a50b8a44510eda55926b948b6524fa13a34c7
Reviewed-on: https://chromium-review.googlesource.com/c/infra/luci/recipes-py/+/5516321
Commit-Queue: Chan Li <chanli@chromium.org>
Auto-Submit: Rob Mohr <mohrr@google.com>
Reviewed-by: Chan Li <chanli@chromium.org>
diff --git a/README.recipes.md b/README.recipes.md
index d44aac4..4d7c241 100644
--- a/README.recipes.md
+++ b/README.recipes.md
@@ -4621,12 +4621,12 @@
 
 Returns current timestamp as a float number of seconds since epoch.
 
-&mdash; **def [timeout](/recipe_modules/time/api.py#218)(self, seconds: float):**
+&mdash; **def [timeout](/recipe_modules/time/api.py#218)(self, seconds: ((float | int) | datetime.timedelta)=None):**
 
-Provides a context that times out after the given number of seconds.
+Provides a context that times out after the given time.
 
 Usage:
-with api.time.timeout(45):
+with api.time.timeout(datetime.timedelta(minutes=5)):
   # your steps
 
 Look at the "deadline" section of https://chromium.googlesource.com/infra/luci/luci-py/+/HEAD/client/LUCI_CONTEXT.md
diff --git a/recipe_modules/time/api.py b/recipe_modules/time/api.py
index 2ebf51e..2309fdc 100644
--- a/recipe_modules/time/api.py
+++ b/recipe_modules/time/api.py
@@ -215,16 +215,20 @@
       random_func = self.m.random.random
     return seconds * (1 + random_func() * (jitter_amount * 2) - jitter_amount)
 
-  def timeout(self, seconds: float):
-    """Provides a context that times out after the given number of seconds.
+  def timeout(self, seconds: float | int | datetime.timedelta = None):
+    """Provides a context that times out after the given time.
 
     Usage:
-    with api.time.timeout(45):
+    with api.time.timeout(datetime.timedelta(minutes=5)):
       # your steps
 
     Look at the "deadline" section of https://chromium.googlesource.com/infra/luci/luci-py/+/HEAD/client/LUCI_CONTEXT.md
     to see how this works.
     """
+
+    if isinstance(seconds, datetime.timedelta):
+      seconds = seconds.total_seconds()
+
     if seconds < 0:
       raise recipe_api.StepFailure('`seconds` cannot be negative')
     current_time = self.time()
diff --git a/recipe_modules/time/examples/full.expected/defaults.json b/recipe_modules/time/examples/full.expected/defaults.json
index df84ce8..ee9718a 100644
--- a/recipe_modules/time/examples/full.expected/defaults.json
+++ b/recipe_modules/time/examples/full.expected/defaults.json
@@ -24,6 +24,19 @@
     "name": "timeout step"
   },
   {
+    "cmd": [
+      "echo",
+      "\"hello\""
+    ],
+    "luci_context": {
+      "deadline": {
+        "grace_period": 30.0,
+        "soft_deadline": 1337000127.5
+      }
+    },
+    "name": "timeout step (2)"
+  },
+  {
     "name": "$result"
   }
 ]
\ No newline at end of file
diff --git a/recipe_modules/time/examples/full.expected/seed_and_step.json b/recipe_modules/time/examples/full.expected/seed_and_step.json
index 78519ee..f7c351b 100644
--- a/recipe_modules/time/examples/full.expected/seed_and_step.json
+++ b/recipe_modules/time/examples/full.expected/seed_and_step.json
@@ -24,6 +24,19 @@
     "name": "timeout step"
   },
   {
+    "cmd": [
+      "echo",
+      "\"hello\""
+    ],
+    "luci_context": {
+      "deadline": {
+        "grace_period": 30.0,
+        "soft_deadline": 253.0
+      }
+    },
+    "name": "timeout step (2)"
+  },
+  {
     "name": "$result"
   }
 ]
\ No newline at end of file
diff --git a/recipe_modules/time/examples/full.py b/recipe_modules/time/examples/full.py
index 39c1bfd..c5317c9 100644
--- a/recipe_modules/time/examples/full.py
+++ b/recipe_modules/time/examples/full.py
@@ -76,6 +76,9 @@
   with api.time.timeout(seconds=100.5):
     api.step('timeout step', ['echo', '"hello"'])
 
+  with api.time.timeout(datetime.timedelta(minutes=2)):
+    api.step('timeout step', ['echo', '"hello"'])
+
   with api.assertions.assertRaises(StepFailure):
     api.time.timeout(seconds=-1.)